package de.hsb.ai.agent;

import jade.content.onto.basic.Action;
import jade.core.AID;
import jade.core.Agent;
import jade.core.ContainerID;
import jade.domain.AMSService;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.domain.JADEAgentManagement.CreateAgent;
import jade.domain.JADEAgentManagement.KillAgent;
import jade.domain.introspection.AMSSubscriber;
import jade.domain.introspection.BornAgent;
import jade.domain.introspection.DeadAgent;
import jade.domain.introspection.Event;
import jade.domain.introspection.IntrospectionVocabulary;
import jade.domain.introspection.ResumedAgent;
import jade.domain.introspection.SuspendedAgent;
import jade.lang.acl.ACLMessage;
import jade.tools.rma.StartDialog;
import jade.wrapper.ControllerException;
import jade.wrapper.StaleProxyException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.SwingUtilities;

import org.apache.log4j.Logger;

import de.hsb.ai.domain.DummyAID;
import de.hsb.ai.domain.GameInfo;
import de.hsb.ai.domain.ScotlandJadeVocabulary;
import de.hsb.ai.ui.ManagementGui;
import de.hsb.ai.util.ScotlandJadeHelper;

/**
 * ScotlandyardManagementAgent keeps track of all registered players and masters.
 */
@SuppressWarnings("serial")
public class ScotlandJadeManagementAgent extends AbstractAgent implements AgentManager {

	public static final String SERVICE_TYPE = "sy.manager";

	private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SERVICE_TYPE);
	
	private List<EventHandler> listeners = new LinkedList<EventHandler>();

	private Map<AID, DFAgentDescription> agents = Collections.synchronizedMap(new HashMap<AID, DFAgentDescription>());

	private ScotlandJadeMasterAgent master;
	
	private ManagementGui gui;

	private Comparator<AID> playerComparator = new Comparator<AID>() {

		@Override
		public int compare(AID p1, AID p2) {
			return p1.getLocalName().compareTo(p2.getLocalName());
		}
		
	};
	
	/**
	 * ScotlandyardManagmentAgent constructor creats & initialises a managment agent object
	 */
	public ScotlandJadeManagementAgent() {
		super(SERVICE_TYPE, SERVICE_TYPE);
		
		ScotlandJadeHelper.learnAMS(this);
		// Adds EventHandler
		addHandler(new EventHandler() {
			
			private Logger eventLogger = Logger.getLogger(logger.getName() + ".event");

			@Override
			public void handle(DFAgentDescription agent, Event e) {
				eventLogger.info(e.toString() + " " + agent.getName().getLocalName());
			}
			
		});
		
		initGui();
	}
	
	/**
	 * Adds the specified EventHandler.
	 * @param listener EventHandler to be added
	 */
	public void addHandler(EventHandler listener) {
		if (listeners.add(listener) && !agents.isEmpty()) {
			synchronized (agents) {
				for (DFAgentDescription agent : agents.values()) {
					listener.handle(agent, EventHandler.Event.ADDED);
				}
			}
		}
	}
	
	/**
	 * Removes the specified EventHandler.
	 * @param listener EventHandler to be removed
	 */
	public void removeHandler(EventHandler listener) {
		listeners.remove(listener);
	}
	
	public void startGame(List<AID> controlledPlayers) {
		if (master != null) {
			GameInfo info = new GameInfo();
			
			Iterator<AID> controlledPlayersIterator = controlledPlayers.iterator();
			
			List<AID> players = new ArrayList<AID>(ScotlandJadeVocabulary.MAX_PLAYERS);
			
			while (controlledPlayersIterator.hasNext()) {
				AID player = controlledPlayersIterator.next();
				jade.util.leap.Iterator iter = agents.get(player).getAllServices();
				while (iter.hasNext()) {
					ServiceDescription sd = (ServiceDescription) iter.next();
					if (sd.getName().contains(AbstractFugitive.SERVICE_NAME_PREFIX)) {
						if (players.size() > 0) {
							throw new IllegalArgumentException("Only one fugitive per game is allowed");
						}
						controlledPlayersIterator.remove();
						players.add(player);
					}
				}
			}
			
			if (players.size() == 0) {
				players.add(new DummyAID());
			}
			
			Collections.sort(controlledPlayers, playerComparator);
			players.addAll(controlledPlayers);
			info.setPlayers(players);
			
			master.startGame(info, ScotlandJadeHelper.config.getRuns());
		}
	}

	public void createAgent() {
		try {
			int res = StartDialog.showStartNewDialog(getContainerController().getContainerName(), gui);
			if (res != StartDialog.OK_BUTTON) return;
		} catch (ControllerException e) {
			logger.error("start-dialog/getting container", e);
			return;
		}
		
		ACLMessage msg = ScotlandJadeHelper.createAMSRequest(this);
	
		CreateAgent ca = new CreateAgent();
		ca.setAgentName(StartDialog.getAgentName());
		ca.setClassName(StartDialog.getClassName());
		ca.setContainer(new ContainerID(StartDialog.getContainer(), null));
		
		Action action = new Action(getAMS(), ca);
		try {
			getContentManager().fillContent(msg, action);
			AMSService.doFipaRequestClient(this, msg, 1000);
		} catch (Exception e) {
			logger.warn("create agent " + StartDialog.getAgentName() + " via AMS failed: " + e.getMessage(), e);
			try {
				createAgentManually(ca);
			} catch (Exception e1) {
				logger.error("manual creation failed as well: " + e1.getMessage(), e);
			}
		}
	}

	private void createAgentManually(CreateAgent ca) throws Exception {
		Class<?> agentClazz = getClass().getClassLoader().loadClass(ca.getClassName());
		Agent agentInstance = (Agent) agentClazz.newInstance();
		getContainerController().acceptNewAgent(ca.getAgentName(), agentInstance).start();
	}

	public void deleteAgent(AID aid) {
		ACLMessage msg = ScotlandJadeHelper.createAMSRequest(this);
		
		KillAgent ka = new KillAgent();
		ka.setAgent(aid);
		
		Action action = new Action(getAMS(), ka);
		try{
			getContentManager().fillContent(msg, action);
			AMSService.doFipaRequestClient(this, msg, 1000);
		} catch (Exception e) {
			logger.error("kill agent", e);
		}
	}
	
	@Override
	public void shutdown() {
		try {
			getContainerController().getPlatformController().kill();
		} catch (ControllerException e) {
			logger.error("shutdown", e);
		}
	}

	@Override
	protected void setup() {
		super.setup();
		master = new ScotlandJadeMasterAgent();
		try {
			getContainerController().acceptNewAgent("master", master).start();
		} catch (StaleProxyException e) {
			logger.error("creating master", e);
		}
		addBehaviour(new Subscriber());
	}
	/**
	 * Initialisation of GUI via Runnable Interface
	 */
	private void initGui() {
		SwingUtilities.invokeLater(new Runnable() {
			
			@Override
			public void run() {
				gui = new ManagementGui(ScotlandJadeManagementAgent.this);
				gui.setVisible(true);
			}
			
		});
	}

	private void notifyListeners(DFAgentDescription info, EventHandler.Event event) {
		for (EventHandler listener : listeners) {
			listener.handle(info, event);
		}
	}
	
	/**
	 * Registers {@code agent} and notifies listeners. 
	 * @param agent Agent to be registered
	 */
	private void register(AID agent) {
		if (agent.equals(getAID())) return;
		
		DFAgentDescription service = null;
		
		if ((service = getService(agent, AbstractPlayer.SERVICE_TYPE)) != null) {
			agents.put(agent, service);
			notifyListeners(service, EventHandler.Event.ADDED);
		}
	}
	
	/**
	 * Deregisters {@code agent} and notifies listeners.
	 * @param agent Agent to be deregistered
	 */
	private void deregister(AID agent) {
		if (master != null && master.getName().equals(agent)) {
			master = null;
		}
		
		DFAgentDescription info = agents.remove(agent);
		if (info != null) {
			notifyListeners(info, EventHandler.Event.REMOVED);
		}
	}

	/**
	 * Notifies listeners about suspended agent.
	 * @param aid the suspended agent
	 */
	private void suspend(AID aid) {
		DFAgentDescription info = agents.get(aid);
		if (info != null) {
			notifyListeners(info, EventHandler.Event.SUSPENDED);
		}
	}

	/**
	 * Notifies listeners about resumed agent.
	 * @param aid the resumed agent
	 */
	private void resume(AID aid) {
		DFAgentDescription info = agents.get(aid);
		if (info != null) {
			notifyListeners(info, EventHandler.Event.RESUMED);
		}
	}

	private DFAgentDescription getService(AID aid, String type) {
		// Creats agent description
		DFAgentDescription ad = new DFAgentDescription();
		ServiceDescription sd = ScotlandJadeHelper.createServiceTemplate();
		sd.setType(type);
		ad.addServices(sd);
		ad.setName(aid);

		try {
			DFAgentDescription[] res = DFService.search(this, ad);
			return res == null || res.length == 0 ? null : res[0];
		} catch (FIPAException e) {
			logger.error("search master", e);
		}
		return null;
	}

	/**
	 * Subscriber subscribes to AMS events. 
	 */
	private class Subscriber extends AMSSubscriber {
		
		@Override
		@SuppressWarnings({ "unchecked", "rawtypes" })
		protected void installHandlers(Map handlersTable) {
			handlersTable.put(IntrospectionVocabulary.BORNAGENT, new AMSSubscriber.EventHandler() {

				@Override
				public void handle(Event ev) { register(((BornAgent) ev).getAgent()); }

			});
			
			handlersTable.put(IntrospectionVocabulary.DEADAGENT, new AMSSubscriber.EventHandler() {
				
				@Override
				public void handle(Event ev) { deregister(((DeadAgent) ev).getAgent()); }
				
			});
			
			handlersTable.put(IntrospectionVocabulary.SUSPENDEDAGENT, new AMSSubscriber.EventHandler() {
				
				@Override
				public void handle(Event ev) { suspend(((SuspendedAgent) ev).getAgent()); }
				
			});
			
			handlersTable.put(IntrospectionVocabulary.RESUMEDAGENT, new AMSSubscriber.EventHandler() {
				
				@Override
				public void handle(Event ev) { resume(((ResumedAgent) ev).getAgent()); }
				
			});
		}
		
	}

	public static interface EventHandler {

		enum Event {
			ADDED, REMOVED, SUSPENDED, RESUMED
		}
		
		void handle(DFAgentDescription agent, Event e);
		
	};
	
}
